﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using epk23.PartIIProject.Exceptions;
using epk23.PartIIProject;
using Tomers.WPF.Shapes;
using System.Windows.Controls;
using System.Windows.Shapes;
using System.Windows.Input;
using System.Windows;

namespace epk23.PartIIProject
{
    public class MachineGraph : ICloneable
    {
        public Machine machine { get; set; }
        public static Canvas canvas { get; set; }
        public static MachineWindow window { get; set; }

        public List<RuleNode> nodes { get; set; }

        public List<GraphArrow> arrows { get; set; }

        public Ellipse startMarker { get; set; }

        public List<bool> ruleNumbers { get; set; }     //boolean list representing available rule numbers (true == available)


        private bool movingArrow;
        private GraphArrow clickedArrow;

        private bool draggingNode;
        public bool settingStart;

        private ConcreteNode dragNode;
        private Type dragNodeType;
        private Point dragOldPosition;
        private Point dragStartPosition;

        private UIAction currentAction;
        private Stack<UIAction> undoStack;
        private Stack<UIAction> redoStack;

        public MachineGraph()
        {
            machine = new Machine();
            nodes = new List<RuleNode>();
            ruleNumbers = new List<bool>();
            arrows = new List<GraphArrow>();
            undoStack = new Stack<UIAction>();
            redoStack = new Stack<UIAction>();
        }

        public void makeStartMarker()
        {
            startMarker = new Ellipse();
            startMarker.Stroke = System.Windows.Media.Brushes.Red;
            startMarker.Fill = System.Windows.Media.Brushes.Red;
            startMarker.Width = 10;
            startMarker.Height = 10;
            startMarker.SetValue(Canvas.LeftProperty, 0.0);
            startMarker.SetValue(Canvas.TopProperty, 0.0);
            startMarker.SetValue(Canvas.ZIndexProperty, Int32.MaxValue);
            startMarker.IsHitTestVisible = true;

            canvas.Children.Add(startMarker);
        }

        public void setStartMarker(RuleNode node)
        {
            startMarker.SetValue(Canvas.LeftProperty, node.x);
            startMarker.SetValue(Canvas.TopProperty, node.y);
        }

        private void refreshStartMarker()
        {
            foreach(ConcreteNode node in nodes)
            if (node.getRule().ruleNumber == machine.startRuleNumber)
                setStartMarker((RuleNode)node);
        }

        public void refreshGraph()
        {
            canvas.Children.Clear();
            foreach (RuleNode node in nodes)
                node.addToCanvas();

            foreach (GraphArrow arrow in arrows)
                canvas.Children.Add(arrow.arrow);

            makeStartMarker();
            refreshStartMarker();
            refreshArrows();
        }

        public void addIncrRuleNode(double x, double y, string regName, Canvas canvas)
        {
            MachineGraph oldGraph = (MachineGraph)this.Clone();

            IncrRuleNode newNode = new IncrRuleNode(x, y, regName, canvas);
            addNode(newNode);
            newNode.addToCanvas();

            makeDefaultArrows(newNode);

            if (newNode.rule.ruleNumber == 0)
                setStartMarker(newNode);

            machine.addRule(newNode.getRule());

            MachineGraph newGraph = (MachineGraph)this.Clone();
            pushAddRemoveNodeAction(oldGraph, newGraph);
        }

        public void addDecrRuleNode(double x, double y, string regName, Canvas canvas)
        {
            MachineGraph oldGraph = (MachineGraph)this.Clone();


            DecrRuleNode newNode = new DecrRuleNode(x, y, regName, canvas);
            addNode(newNode);
            newNode.addToCanvas();

            makeDefaultArrows(newNode);

            if (newNode.rule.ruleNumber == 0)
                setStartMarker(newNode);

            machine.addRule(newNode.getRule());

            MachineGraph newGraph = (MachineGraph)this.Clone();
            pushAddRemoveNodeAction(oldGraph, newGraph);
        }

        public void addHaltRuleNode(double x, double y, Canvas canvas)
        {
            MachineGraph oldGraph = (MachineGraph)this.Clone();

            HaltRuleNode newNode = new HaltRuleNode(x, y, canvas);
            addNode(newNode);
            newNode.addToCanvas();

            if (newNode.rule.ruleNumber == 0)
                setStartMarker(newNode);

            machine.addRule(newNode.getRule());

            MachineGraph newGraph = (MachineGraph)this.Clone();
            pushAddRemoveNodeAction(oldGraph, newGraph);
        }

        public void addMachineNode(double x, double y, string machineName, Canvas canvas)
        {
            MachineGraph oldGraph = (MachineGraph)this.Clone();

            MachineNode newNode = new MachineNode(x, y, machineName, canvas);
            addNode(newNode);
            newNode.addToCanvas();

            makeDefaultArrows(newNode);

            foreach (String s in FileManager.readFile(machineName).Machine.getRegisters())
                machine.addRegister(s);

            machine.addRule(newNode.getRule());

            MachineGraph newGraph = (MachineGraph)this.Clone();
            pushAddRemoveNodeAction(oldGraph, newGraph);
        }

        public void addNode(RuleNode n)
        {
            nodes.Add(n);
        }

        public void addArrow(GraphArrow gArrow)
        {
            arrows.Add(gArrow);
            canvas.Children.Add(gArrow.arrow);
        }

        public void deleteNode(ConcreteNode nodeToDelete)
        {
            MachineGraph oldGraph = (MachineGraph)this.Clone();

            nodes.Remove((RuleNode)nodeToDelete);
            nodeToDelete.delete();
            Rule ruleToDelete = nodeToDelete.getRule();
            machine.removeRule(ruleToDelete);
            ruleNumbers[ruleToDelete.ruleNumber] = true;

            if (nodeToDelete.getRule().ruleNumber == machine.startRuleNumber)
            {
                machine.startRuleNumber = 0;
            }

            bool foundStartNode = false;
            foreach (ConcreteNode node in nodes)
            {
                if (node.getRule().ruleNumber == machine.startRuleNumber)
                {
                    setStartMarker((RuleNode)node);
                    foundStartNode = true;
                }
            }
            if (!foundStartNode)
            {
                startMarker.SetValue(Canvas.LeftProperty, 10.0);
                startMarker.SetValue(Canvas.TopProperty, 10.0);
            }

            MachineGraph newGraph = (MachineGraph)this.Clone();
            pushAddRemoveNodeAction(oldGraph, newGraph);
        }

        private void pushAddRemoveNodeAction(MachineGraph oldGraph, MachineGraph newGraph)
        {
            currentAction = new AddRemoveNodeAction(oldGraph, newGraph, window, canvas);
            undoStack.Push(currentAction);
        }

        public int getNextRuleNumber()
        {
            bool found = false;
            int n = -1;
            int i = 0;
            foreach (bool b in ruleNumbers)
            {
                if (b == true)
                {
                    n = i;
                    found = true;
                    break;
                }

                i++;
            }
            if (found)
                ruleNumbers[n] = false;
            else
            {
                ruleNumbers.Add(false);
                n = ruleNumbers.Count - 1;
            }

            if (n < 0)
                throw new InvalidRuleNumberException();

            return n;
        }

        public ConcreteNode lookupNode(int ruleNumber)
        {
            ConcreteNode returnNode = null;

            foreach (ConcreteNode node in nodes)
            {
                if (node.getRule().ruleNumber == ruleNumber)
                {
                    returnNode = node;
                    break;
                }
            }
            if (returnNode == null)
                throw new NodeNotFoundException();

            return returnNode;
        }

        public void makeDefaultArrows(ConcreteNode node)
        {
            Arrow newArrow = makeNewArrow();

            GraphArrow newGraphArrow = new GraphArrow((RuleNode)node, (RuleNode)node, newArrow, 0);
            addArrow(newGraphArrow);

            if (node.GetType() == typeof(DecrRuleNode))
            {
                Arrow newArrow2 = makeNewArrow();
                newArrow2.StrokeThickness = Values.ArrowType2Thickness;
                GraphArrow newGraphArrow2 = new GraphArrow((RuleNode)node, (RuleNode)node, newArrow2, 1);
                addArrow(newGraphArrow2);
            }
        }

        private Arrow makeNewArrow()
        {
            Arrow newArrow = new Arrow();
            newArrow.MouseDown += arrowMouseDown;
            newArrow.Stroke = System.Windows.Media.Brushes.Black;
            newArrow.StrokeThickness = 5;
            newArrow.HeadWidth = 5;
            newArrow.HeadHeight = 5;
            newArrow.SetValue(Canvas.ZIndexProperty, 10);

            return newArrow;
        }

        public void setArrows(ConcreteNode node) //sets arrows pointing FROM this node 
        {                                        //to their correct end nodes
            foreach (GraphArrow arrow in arrows)
            {
                if (arrow.startNode == node)
                {
                    int targetRuleNumber = node.getTargetRuleNumber();
                    arrow.setEndNode((RuleNode)lookupNode(targetRuleNumber));
                }
            }
        }

        public void arrowMouseDown(object sender, MouseButtonEventArgs e)
        {
            movingArrow = true;
            foreach (GraphArrow graphArrow in arrows)
            {
                if (graphArrow.arrow == sender)
                    clickedArrow = graphArrow;
            }
        }

        public void nodeBodyMouseDown(object sender, MouseButtonEventArgs e)
        {
            dragNode = this.getNode(sender);
            dragNodeType = dragNode.GetType();
            dragOldPosition = e.GetPosition(canvas);
            dragStartPosition = new Point(((RuleNode)dragNode).x, ((RuleNode)dragNode).y);
            draggingNode = true;
        }

        public void nodeBodyMouseUp(object sender, MouseButtonEventArgs e)
        {
            if (draggingNode)
            {
                draggingNode = false;
                RuleNode draggedNode = (RuleNode)dragNode;
                currentAction = new MoveNodeAction(dragNode, dragStartPosition, new Point(draggedNode.x, draggedNode.y), this);
            }
            if (settingStart)
            {
                settingStart = false;

                RuleNode oldStartNode = null;
                foreach (ConcreteNode node in nodes)
                {
                    if (node.getRule().ruleNumber == machine.startRuleNumber)
                        oldStartNode = (RuleNode)node;
                }

                RuleNode newStartNode = (RuleNode)getNode(sender);
                setStartNode(newStartNode);

                currentAction = new SetStartNodeAction(oldStartNode, newStartNode, this);
            }

            if (movingArrow)
            {
                movingArrow = false;
                RuleNode oldEndNode = clickedArrow.endNode;
                RuleNode newEndNode = (RuleNode)getNode(sender);
                moveArrow(clickedArrow, newEndNode);

                currentAction = new MoveArrowAction(clickedArrow, oldEndNode, clickedArrow.endNode);
            }

            undoStack.Push(currentAction);
        }

        public void nodeBodyMouseMove(object sender, MouseEventArgs e)
        {
            if (draggingNode)
            {
                Point currentPosition = e.GetPosition(canvas);
                double cx = (double)((RuleNode)dragNode).x;
                double cy = (double)((RuleNode)dragNode).y;

                double newX = cx + currentPosition.X - dragOldPosition.X;
                double newY = cy + currentPosition.Y - dragOldPosition.Y;

                if (cx < 0)
                {
                    dragNode.setPosition(0, newY);
                    draggingNode = false;
                }
                else if (cy < 0)
                {
                    dragNode.setPosition(newX, 0);
                    draggingNode = false;
                }
                else if (cx + ((RuleNode)dragNode).nodeBody.Width > canvas.Width)
                {
                    moveNode(dragNode, canvas.Width - ((RuleNode)dragNode).nodeBody.Width, newY);
                    draggingNode = false;
                }
                else if (cy + ((RuleNode)dragNode).nodeBody.Height > canvas.Height)
                {
                    moveNode(dragNode, newX, canvas.Height - ((RuleNode)dragNode).nodeBody.Height);
                    draggingNode = false;
                }
                else
                {
                    moveNode(dragNode, newX, newY);
                    dragOldPosition = currentPosition;
                }
            }
        }

        public void nodeBodyMouseEnter(object sender, MouseEventArgs e)
        {
            RuleNode mousedNode = (RuleNode)this.getNode(sender);
            mousedNode.deleteLabel.Visibility = System.Windows.Visibility.Visible;
        }

        public void nodeBodyMouseLeave(object sender, MouseEventArgs e)
        {
            Point mousePosition = e.GetPosition(canvas);
            RuleNode mousedNode = (RuleNode)this.getNode(sender);
            Label deleteLabel = mousedNode.deleteLabel;
            double labelX = Canvas.GetLeft(deleteLabel);
            double labelY = Canvas.GetTop(deleteLabel);
            int labelSize = Values.DeleteLabelSize;

            if (!(mousePosition.X > labelX && mousePosition.X < labelX + labelSize && mousePosition.Y > labelY && mousePosition.Y < labelY + labelSize))
                ((RuleNode)this.getNode(sender)).deleteLabel.Visibility = System.Windows.Visibility.Hidden;
        }

        public void deleteLabelMouseUp(object sender, MouseButtonEventArgs e)
        {
            ConcreteNode nodeToDelete = ((ConcreteNode)this.getNode(sender));
            deleteNode(nodeToDelete);
        }

        public void setStartNode(RuleNode newStartNode)
        {
            int newStartRuleNumber = ((ConcreteNode)newStartNode).getRule().ruleNumber;
            machine.startRuleNumber = newStartRuleNumber;
            setStartMarker(newStartNode);
        }

        private void moveArrow(GraphArrow movedArrow, RuleNode newEndNode)
        {

            clickedArrow.setEndNode(newEndNode);

            if (movedArrow.startNode.GetType() == typeof(IncrRuleNode))
            {
                ((IncrRuleNode)movedArrow.startNode).rule.targetRuleNumber = ((ConcreteNode)newEndNode).getRule().ruleNumber;
            }
            else if (movedArrow.startNode.GetType() == typeof(MachineNode))
            {
                ((MachineNode)movedArrow.startNode).rule.targetRuleNumber = ((ConcreteNode)newEndNode).getRule().ruleNumber;
            }
            else if (movedArrow.startNode.GetType() == typeof(DecrRuleNode))
            {
                if (movedArrow.type == 1)
                {
                    ((DecrRuleNode)movedArrow.startNode).rule.targetRuleNumber = ((ConcreteNode)newEndNode).getRule().ruleNumber;
                }
                else if (movedArrow.type == 0)
                {
                    ((DecrRuleNode)movedArrow.startNode).rule.targetRuleNumber2 = ((ConcreteNode)newEndNode).getRule().ruleNumber;
                }
            }
        }

        public void moveNode(ConcreteNode node, double x, double y)
        {
            node.setPosition(x, y);
            refreshArrows();
            if (node.getRule().ruleNumber == machine.startRuleNumber)
                setStartMarker((RuleNode)node);
        }

        private void refreshArrows()
        {
            foreach (GraphArrow arrow in arrows)
            {
                arrow.setPosition();
            }
        }

        public static MachineGraph loadGraph(string machineName, MachineGraph graph)
        {

            graph.nodes.Clear();
            graph.arrows.Clear();
            graph.ruleNumbers = new List<bool>();

            MachineLayout layout = FileManager.readFile(machineName);

            Machine m = layout.Machine;
            List<int[]> layoutList = layout.Layout;

            graph.machine = m;

            foreach (Rule r in m.rules)
            {
                double x = layout.getPosition(r.ruleNumber).X;
                double y = layout.getPosition(r.ruleNumber).Y;


                if (r.GetType() == typeof(IncrementRule))
                {
                    IncrRuleNode newNode = new IncrRuleNode(x, y, r.targetRegister, canvas);
                    newNode.getRule().ruleNumber = r.ruleNumber;
                    graph.addNode(newNode);
                    newNode.addToCanvas();
                }

                else if (r.GetType() == typeof(DecrementRule))
                {
                    DecrRuleNode newNode = new DecrRuleNode(x, y, r.targetRegister, canvas);
                    newNode.getRule().ruleNumber = r.ruleNumber;
                    graph.addNode(newNode);
                    newNode.addToCanvas();
                }
                else if (r.GetType() == typeof(HaltRule))
                {
                    HaltRuleNode newNode = new HaltRuleNode(x, y, canvas);
                    newNode.getRule().ruleNumber = r.ruleNumber;
                    graph.addNode(newNode);
                    newNode.addToCanvas();
                }
                else if (r.GetType() == typeof(MachineRule))
                {
                    MachineNode newNode = new MachineNode(x, y, ((MachineRule)r).machineName, canvas);
                    newNode.getRule().ruleNumber = r.ruleNumber;
                    graph.addNode(newNode);
                    newNode.addToCanvas();
                    //List<String> componentMachineRegisters = getComponentMachineRegisters(fileManager.readFile(((MachineRule)r).machineName).Machine);
                }

            }

            List<String> regNames = m.getRegisters();

            foreach (String s in regNames)
                m.addRegister(s);

            foreach (Rule r in m.rules)
            {
                if (r.GetType() == typeof(IncrementRule))
                {
                    RuleNode startNode = (RuleNode)graph.lookupNode(r.ruleNumber);
                    RuleNode endNode = (RuleNode)graph.lookupNode(((IncrementRule)r).targetRuleNumber);
                    Arrow newArrow = graph.makeNewArrow();
                    graph.addArrow(new GraphArrow(startNode, endNode, newArrow, 0));
                }
                else if (r.GetType() == typeof(MachineRule))
                {
                    RuleNode startNode = (RuleNode)graph.lookupNode(r.ruleNumber);
                    RuleNode endNode = (RuleNode)graph.lookupNode(((MachineRule)r).targetRuleNumber);
                    Arrow newArrow = graph.makeNewArrow();
                    graph.addArrow(new GraphArrow(startNode, endNode, newArrow, 0));
                }
                else if (r.GetType() == typeof(DecrementRule))
                {
                    RuleNode startNode = (RuleNode)graph.lookupNode(r.ruleNumber);
                    RuleNode endNode1 = (RuleNode)graph.lookupNode(((DecrementRule)r).targetRuleNumber);
                    RuleNode endNode2 = (RuleNode)graph.lookupNode(((DecrementRule)r).targetRuleNumber2);
                    Arrow newArrow1 = graph.makeNewArrow();
                    Arrow newArrow2 = graph.makeNewArrow();
                    newArrow1.StrokeThickness = 7;
                    graph.addArrow(new GraphArrow(startNode, endNode1, newArrow1, 1));
                    graph.addArrow(new GraphArrow(startNode, endNode2, newArrow2, 0));
                }

            }

            return graph;
        }

        private ConcreteNode getNode(Object o)
        {
            ConcreteNode clickedNode = null;
            foreach (RuleNode r in nodes)
            {
                if (r.nodeBody == o || r.deleteLabel == o)
                    clickedNode = (ConcreteNode)r;
            }
            return clickedNode;
        }


        public object Clone()
        {
            MachineGraph newGraph = new MachineGraph();

            foreach (RuleNode node in nodes)
            {
                newGraph.nodes.Add((RuleNode)node.Clone());

                node.nodeBody.MouseDown -= (MouseButtonEventHandler)this.nodeBodyMouseDown;
                node.nodeBody.MouseMove -= (MouseEventHandler)this.nodeBodyMouseMove;
                node.nodeBody.MouseUp -= (MouseButtonEventHandler)this.nodeBodyMouseUp;
                node.nodeBody.MouseEnter -= (MouseEventHandler)this.nodeBodyMouseEnter;
                node.nodeBody.MouseLeave -= (MouseEventHandler)this.nodeBodyMouseLeave;
                node.deleteLabel.MouseUp -= (MouseButtonEventHandler)this.deleteLabelMouseUp;

                node.nodeBody.MouseDown += (MouseButtonEventHandler)newGraph.nodeBodyMouseDown;
                node.nodeBody.MouseMove += (MouseEventHandler)newGraph.nodeBodyMouseMove;
                node.nodeBody.MouseUp += (MouseButtonEventHandler)newGraph.nodeBodyMouseUp;
                node.nodeBody.MouseEnter += (MouseEventHandler)newGraph.nodeBodyMouseEnter;
                node.nodeBody.MouseLeave += (MouseEventHandler)newGraph.nodeBodyMouseLeave;
                node.deleteLabel.MouseUp += (MouseButtonEventHandler)newGraph.deleteLabelMouseUp;
            }

            foreach (GraphArrow graphArrow in arrows)
            {
                newGraph.arrows.Add(graphArrow);

                graphArrow.arrow.MouseDown -= (MouseButtonEventHandler)this.arrowMouseDown;
                graphArrow.arrow.MouseDown += (MouseButtonEventHandler)newGraph.arrowMouseDown;
            }

            //foreach (RuleNode node in newGraph.nodes)
            //{
            //    if(node.GetType() == typeof(IncrRuleNode) || node.GetType() == typeof(DecrRuleNode))
            //    {
            //        Arrow newArrow = makeNewArrow();
            //        RuleNode endNode = (RuleNode)lookupNode(((ConcreteNode)node).getTargetRuleNumber());
            //        GraphArrow newGraphArrow = new GraphArrow(node, endNode, newArrow, 0);

            //        newGraph.arrows.Add(newGraphArrow);
            //    }

            //    if(node.GetType() == typeof(DecrRuleNode))
            //    {
            //        Arrow newArrow2 = makeNewArrow();
            //        newArrow2.StrokeThickness = Values.ArrowType2Thickness;
            //        DecrementRule rule = ((DecrRuleNode)node).rule;
            //        int targetRule2 = rule.targetRuleNumber2;

            //        RuleNode endNode = (RuleNode)lookupNode(targetRule2);
            //        GraphArrow newGraphArrow2 = new GraphArrow(node, endNode, newArrow2, 1);

            //        newGraph.arrows.Add(newGraphArrow2);
            //    }
            //}

            foreach (bool b in ruleNumbers)
                newGraph.ruleNumbers.Add(b);

            newGraph.machine = (Machine)machine.Clone();


            return newGraph;
        }

        public void undo()
        {
            if (undoStack.Count > 0)
            {
                UIAction action = undoStack.Pop();
                action.undo();
                redoStack.Push(action);
            }
        }

        public void redo()
        {
            if (redoStack.Count > 0)
            {
                UIAction action = redoStack.Pop();
                action.redo();
                undoStack.Push(action);
            }
        }
    }
}

